/*
==========================================================
DX490a - Summer 2010
Instructor: Stelios Manousakis
==========================================================
Class 7.1:
Interfacing 3: Human Interface Devices (HID) in SuperCollider
Contents:
• General
- Connecting
- Receiving data
- Types of data-slots
- An example: Internal trackpad (MBP)
- Mapping actions
- Naming input-slots
• Example: 3DConnexxion's SpaceNavigator
==========================================================
*/
// ================= HID DEVICES IN SUPERCOLLIDER =================
// ====== GENERAL ======
// HID devices include most computer peripherals: keyboards, mice, gamepads, joysticks and most game controllers in general, as well as some more specialized devices.
// In SuperCollider, there is a cross-platform Class for accessing HID devices: GeneralHID.
// ------ Connecting --
// The first thing you need to do is look at the devices that are connected to your machine:
GeneralHID.buildDeviceList
// You can make a list of the devices:
d = GeneralHID.deviceList;
// or post in a more legible format:
GeneralHID.postDevices;
// You then can pick a specific device and open it for usage:
a = GeneralHID.open(d[2])
a.close; // you can close a device like this
// get info on the device
a.info
// Another useful method for accessing is to refer to the device by name, with the GeneralHID.findBy method. First, get all the arguments like this:
a.info.findArgs
// this info corresponds to: vendor ID, productID, location ID, version ID
d = GeneralHID.findBy(1118, 40, -49152000, 258);
d = GeneralHID.findBy(*a.info.findArgs);
a = GeneralHID.open(d)
// ------ Receiving data --
// IMPORTANT: Once you've established a connection to one or more devices, you need to start the eventloop to query data!
GeneralHID.startEventLoop
// In MacOSX you can also set the rate of the loop, although the default of 0.005, should be fine
GeneralHID.startEventLoop(0.005)
// Now that the device is connected and running, let's look at what it can do:
a.slots;
// or more readable:
a.caps; // capacities
// ------ Types of data-slots --
// there are different types of slots:
// button (type 1): has only on/off (1/0) states
// relative (type 2): counts up or down (scrollwheel for example)
// absolute (type 3): continuous value between 0 and 1:
// some other may show up on Linux (Syn (type 0) and Miscellaneous (type 4)), but these are generally not very useful.
// You can use the debug function to see if data is coming in:
a.debug_(true);
// this takes up some resources, so use it only to have a look, then:
a.debug_(false);
// ------ An example: internal trackpad (MBP) --
// lets get some acceleration data, plus the button action from the internal trackpad (on a MacBookPro):
a.close; // close previous device
a = GeneralHID.open(d[4]); // the location could be different in your machine...
GeneralHID.startEventLoop; // start eventloop if you haven't yet
a.debug_(true);
a.debug_(false);
// you can also check individual slots:
a.slots[3][48].debug_(true); // get acceleration on the X axis. Notice that no acceleration is 0.5, acceleration to the right goes down to 0, and acceleration to the left goes up to 1 - well, in my machine at least...
a.slots[3][48].debug_(false);
// there is also a quick-and-dirty GUI to look at the data:
a.makeGui
// ------ Mapping actions --
// You can use the .action method on a specific slot to map a function to an individual input parameter. Note that to get the value you have to call .value on the function's argument:
a.slots[3][48].action_({|in| "mouse X acceleration is: ".post; (in.value - 0.5).postln});
// To unmap, just use an empty function:
a.slots[3][48].action_({})
// ------ Naming input-slots --
// The GeneralHID class contains a dictionary, so you can give a name to a specific slot and access it thusly - something that can make your code much more readable:
a.add(\mouseAccel_X, [3, 48]);
a.add(\mouseAccel_Y, [3, 49]);
a.add(\mouseButton, [1, 1]);
a[\mouseAccel_X].action_({|in| "mouse X acceleration is: ".post; (in.value - 0.5).postln});
a[\mouseAccel_Y].action_({|in| "mouse Y acceleration is: ".post; (in.value - 0.5).postln});
a[\mouseButton].action_({|in| "mouse Button was ".post; if(in.value == 1, {"Pressed".postln}, {"Released!".postln})});
( // stop it:
a[\mouseAccel_X].action_({});
a[\mouseAccel_Y].action_({});
a[\mouseButton].action_({});
)
// The GeneralHID class also has a method to automatically create control buses for you and send data to a synth running on the server, which can be handy if you're not using Ctk stuff
s.boot;
a[\mouseAccel_X].createBus(s); // you need to define the server where the bus will be created
a[\mouseAccel_Y].createBus(s); // you need to define the server where the bus will be created
SynthDef(\theremin, {|outbus = 0, freq = 220, amp = 0|
freq = ((freq + 0.5) * 1000) + 200; // scale the freq values a bit
amp = (amp - 0.5).abs; // we only care about acceleration, no matter which direction it goes towards
Out.ar(outbus, SinOsc.ar(freq + (freq * SinOsc.ar(7, 0, 0.02)), mul: Lag.kr((amp))))
}).load(s);
// a very weird theremin controller:
t = Synth.new(\theremin)
t.map(\freq, a[\mouseAccel_X].bus);
t.map(\amp, a[\mouseAccel_Y].bus);
t.free
// Although you can do everything you need with the GeneralHID class, there are 3 more specialized classes implementing specific functionalities of that class. You can check their helpfiles:
// ====== Example: 3DConnexxion's SpaceNavigator ======
// The 3DConnexxion SpaceNavigator is an HID device with 6 degrees of freedom and 2 buttons. You need to install drivers in your machine to make it work:
"open http://www.3dconnexion.com/service/drivers.html".unixCmd
// After installing the drivers, you may want to tweak some of the preferences, deactivating the buttons from the system (in System Preferences for OSX), and possibly reversing the axes for convenience (the examples below work with all axes reversed)
// connect to the Space Navigator by name (this works on the device I have in my hands, you may have to find the values yourself with the .info.findArgs method)
GeneralHID.postDevices;
// n = GeneralHID.findBy(1133, 50726, -49213440, 1028);
d = GeneralHID.deviceList;
a = GeneralHID.open(d[2]);
// start the eventloop if its not running already:
GeneralHID.startEventLoop
a.makeGui;
a.slots;
/*
// Here's what the Space Navigator spits out:
IdentityDictionary[
// the buttons
(1 -> IdentityDictionary[
(1 -> GeneralHIDSlot(Button, type: 1, id: 1, value: 0)),
(2 -> GeneralHIDSlot(Button, type: 1, id: 2, value: 0)),
(75 -> GeneralHIDSlot(Button, type: 1, id: 75, value: 0)) ]), // non-existent in hardware!
// the LED
(17 -> IdentityDictionary[
(75 -> GeneralHIDSlot(LED, type: 17, id: 75, value: 0)) ]),
// the 6 axes
(3 -> IdentityDictionary[
(48 -> GeneralHIDSlot(Absolute, type: 3, id: 48, value: 0.5)),
(52 -> GeneralHIDSlot(Absolute, type: 3, id: 52, value: 0.5)),
(53 -> GeneralHIDSlot(Absolute, type: 3, id: 53, value: 0.5)),
(49 -> GeneralHIDSlot(Absolute, type: 3, id: 53, value: 0.5)),
(50 -> GeneralHIDSlot(Absolute, type: 3, id: 53, value: 0.685)),
(51 -> GeneralHIDSlot(Absolute, type: 3, id: 53, value: 0.5))])];
*/
// get the current values:
// what's the current state of the buttons:
a.slots[1][1].value; // button on left of logo
a.slots[1][2].value; // button on right of logo
a.slots[1][75].value; // non-existent in hardware
a.slots[17][75].value; // LED - not a control...
// continuous controls:
a.slots[3][48].value; // pan right/left (right/left movement)
a.slots[3][49].value; // pan up/down (push down / pull up)
a.slots[3][50].value; // zoom (push front / pull back)
a.slots[3][51].value; // tilt (nose down / nose up)
a.slots[3][52].value; // spin (rotate right/left)
a.slots[3][53].value; // roll (right shoulder down / left shoulder down)
// let's make a dictionary for legibility:
(
a.add(\leftBtn, [1, 1]);
a.add(\rightBtn, [1, 2]);
a.add(\r_l_pan, [3, 48]);
a.add(\u_d_pan, [3, 49]);
a.add(\push_pull, [3, 50]);
a.add(\tilt, [3, 51]);
a.add(\spin, [3, 52]);
a.add(\roll, [3, 53]);
)
// Now, let's make some noise!:
(
// load a simple FM synth, no mapping inside the synth
~combfm = CtkSynthDef( \combFM, { |outbus = 0, freq = 440, harm = 1, modix = 1, delay = 0.1, pan = 0, amp = 0.5|
var car, mod, out, dev, modfreq;
modfreq = freq * harm;
dev = modix * modfreq;
mod = SinOsc.ar(modfreq, 0, dev);
car = SinOsc.ar(freq + (mod * modix));
out = Out.ar(outbus, (Pan2.ar(CombC.ar(car, 0.5, delay, 0.5), pan, amp)));
});
// create as many control buses as the parameters of the controlling interface
~ctrl = 6.collect({CtkControl.play});
// create some envelopes to handle the parameter mapping as you wish
~panEnv = Env([-1, 1], [1], \lin);
~ampEnv = Env([0.001, 0.7], [1], 3);
~modEnv = Env([1, 50], [1], 2);
~freqEnv = Env([40, 3000], [1], 4);
//~cutEnv = Env([50, 8000], [1], 2);
~delEnv = Env([0, 0.5], [1], 2);
~harmEnv = Env([0.1, 10], [1], 2);
// within each slot's action: fill the appropriate CtkControl bus with the values coming in from the device slot:
a[\r_l_pan].action_({|val| ~ctrl[0].set([~panEnv[val.value]])});
/* what this does:
1. pick the control slot:
a.slots[3][48]
2. .action returns a value
.action_({|val| ...})
3. which we use as an index to get a value from an Env (used here as a mapping curve)
~panEnv[val.value]
4. and the result is used to set the current value of the control bus
~ctrl[0].set([.....])
*/
a[\u_d_pan].action_({|val| ~ctrl[1].set([~modEnv[val.value]])});
a[\push_pull].action_({|val| ~ctrl[2].set([~ampEnv[val.value]])});
a[\tilt].action_({|val| ~ctrl[3].set([~delEnv[val.value]])});
a[\spin].action_({|val| ~ctrl[4].set([~freqEnv[val.value]])});
a[\roll].action_({|val| ~ctrl[5].set([~harmEnv[val.value]])});
)
// now, start it!:
~note = ~combfm.new().pan_(~ctrl[0]).modix_(~ctrl[1]).amp_(~ctrl[2]).delay_(~ctrl[3]).freq_(~ctrl[4]).harm_(~ctrl[5]).play;
// free it
~note.free